UnityのDebug.Logの話(IL2CPPでログを出したいみたいな話を添えて)
概要
個人的にUnityでのLogが実機(iOSとかIL2CPPの果て)で最終的にどうなるか、という動作を追い切ったことがないんで、追った話。
前提
・UnityEditorのLogging設定では、ログは出る。stacktraceが止むだけ。
・UnityEngine.Debug.unityLogger.logEnabled = false; では、実行時にこのコードを実行後、ログは出なくなる。
ここまででいうと、後者、 UnityEngine.Debug.unityLogger.logEnabled = false; いいじゃんってなると思うんだけど、まだ気になるポイントがあった。
発端
UnityEngine.Debug.unityLogger.logEnabled = false; を使っても、Debug.Logの中の実装が変わるだけで、
Debug.Log(“a:” + 1000);
とかの中身が変わってログは出なくなるが、
“a:” + 1000 の部分は、“a:” + 1000.ToString() みたいな処理が走る。
処理が走るやんけ、というところで、ToStringは new string ~ みたいなのがその結果として発生するので、
結果としてログ関数の中身を無効化しても、無駄なnew発生してるやんけという話。
before
Debug.Log(“a:” + 1000);
after
UnityEngine.Debug.unityLogger.logEnabled = false;
Debug.Log(“a:” + 1000);// 関数の中身がすげ変わってログは出なくなってるが、1000をstringに変える、という処理は残っている。
これはまあ、“a:” + 1000の部分で発生したToString()の成果が、GCの対象となる。
で、まあ、つまり、
・UnityEngine.Debug.unityLogger.logEnabled = false; をやっても、結局ログコード自体が残っていると無駄なnewが走る
というわけだ。俺はこの時点で結構絶望した。
なんでかというと、UnityEngine.Debug.Log類をglobalで上書きする、という、ヒューマンパワーを食ううえに不完全な方法しか解決策がないから。
どう不完全か
この辺がわかりやすい。
【Unity】リリース時にDebug.Logを出力しないようにする
https://noracle.jp/unity-no-debug/
この記事では、
・自前でUnityEngine.Debugを上書きして
・Conditional属性をつけることで、根こそぎ無効にできる
という手法と、その手法がなお及ばない部分がある、と言うことを示してくれている。
・using A=B とかやられてるやつは対処できない(ライブラリとかにめっちゃある)
ということがそれ。コードならいいけど、dllなら? 詰むが?
そしてそれとは別に、まあ、自分はこっちの方が気になるんだけど、
・毎回誰かが同じ、Debug.Log*系のコードを書かないといけない
これなんの罰ゲームだよ。被ったりするしみんなが同じことを毎回やる必要ないでしょ。
そう、つまり不完全なのだよ。
完全な、デザインされた元栓が欲しいんだ。
こうであってほしい
・コンパイル時にUnityEngine.Debug.LogをConditionalで制御できるようにしてほしい
・そしたらコンパイル時にUnityEngine.Debug.Logを呼んでる箇所は、たとえdllの中でも消えてくれると思うし、
・例えばログのためのローカル変数とかもunused扱いになってコンパイルで消せると思うので
みたいな感じ。ユーザーが書けるC#レイヤーでこういう問題に挑ませるの悪手だと思う。
具体例を書こうか。
こういうコードがあったら、
foreach (var path in deleteTargetPaths)
{
Directory.Delete(path, true);
Debug.Log("folder deleted:" + path);
}
もし、Debug.LogがConditionalAttributeとかで実装してあれば、
foreach (var path in deleteTargetPaths)
{
Directory.Delete(path, true);
}
コンパイル時にこうなったりするはずなんだよ。
するとどうだ、"folder deleted:" + path が関数ごと消えるでしょ。こうなれば関数がどうなってようと関係ないじゃん。幸せでは。
IL2CPP側はどうなっているのか
で、まあ、自分の中での結論は↑の通り、UnityEngine.DebugにConditionalつけといてよ、なのだが、
それはまあなんか言う機会を作って言えばいい話なので、なんならバグレポレベルだと思ってるしそれはそれとして。
今できることはなんだろうか。
例えば、IL2CPPへとコンパイルされた段階で、Debug.Log関数の元締めは世界で一個しかないはずだ。
だから、その関数をIL2CPP上から滅殺できる、みたいなマクロがもしラッパーとして定義されていれば、
その部分をいじることでマクロとしてDebug.Logを消し、Log関数に読み込まれている各種string化処理とかconcat処理とかも、
コンパイラが「お前これつかってないやんけ!!」パワーを発揮して滅殺してくれるのでは!???!? という夢があった。
結論から書くとそれは夢だった。マクロなんかなかった。
IL2CPP世界のログの定義はこんな感じのところにある。
PROJECT/iOS/Classes/Native/Bulk_Assembly-CSharp_0.cpp
(行数に注目するとちょっとクスッとくると思う)
さあ、C# UnityEngine.Debug.Logが変換されて出てくるログの関数を探そう。
該当の関数はこれ。
// System.Void UnityEngine.Debug::Log(System.Object)
extern "C" IL2CPP_METHOD_ATTR void Debug_Log_m4051431634 (RuntimeObject * __this /* static, unused */, RuntimeObject * p0, const RuntimeMethod* method);
長えな。
mなんちゃらの数値は、Unityのバージョンに関連なく固定っぽい。特にビルド対象によって変動はしなかった。
そしてこの関数はIL2CPP化されたコードの上で使われてるはずなので、使用箇所を探してみる。
Debug_Log_m4051431634(NULL /*static, unused*/, _stringLiteral223888751, /*hidden argument*/NULL);
こんな感じに、そのままログ関数がコード上で使われている。
マクロとかでラップされてて、マクロを書き換えればあら不思議、なかったことに、、、!! みたいな世界はこの世にはなかった。本当に残念だ。
ここで、「マクロで書かれてれば最悪でもIL2CPPレイヤーでなんとかできるのでは」という夢は霧散する。
IL2CPPでDebug.Logを書こう
最後に、IL2CPPで好きなようにログを書く、という、この世の果てで使えそうなデバッグ手法を紹介しておく。
IL2CPPで出力されたC++コードとは何か、っていうと、あれは、コンパイル結果だ。
断じて人間が何かを書き加えたり、変更を加えられる空間ではない。
で、まあ、
Debug_Log_m4051431634(NULL /*static, unused*/, _stringLiteral223888751, /*hidden argument*/NULL);
とかのコードでは、
_stringLiteral223888751
というポインタにIL2CPP string型みたいなのがあって、そこにC#で書いたC# stringが置かれている。
(実際にはバカでっかい文字列置き場があって、そこにすべての文言が連続する形でキュッと収められている。)
さて、そのIL2CPP string、自力で作ることはできるのか。
できる。
IL2CPP_RUNTIME_CLASS_INIT(Debug_t3317548046_il2cpp_TypeInfo_var);
String_t* a = il2cpp_codegen_string_new_wrapper("comment here!!");
Debug_Log_m4051431634(NULL, (RuntimeObject *)a, NULL);
こんなふうにすれば、comment here!! っていう表示がXcodeとかに出る。
便利でっしゃろ。
IL2CPP世界ではXcodeのbreak pointを置くことでもちろんいい感じにステップ実行できるんだけど、
ポインタのラッパーやラッパーのラッパーがあるせいで、さてここにどうやってジャンプしてきたんだろう、みたいなスタック情報がかき消えているケースが多い。
つまり上から下は見やすいんだけど、
下から上が超絶に見えない。
となると最後の手段はログでしょって感じで、上記のコードが使える。
前提としては、
#include "codegen/il2cpp-codegen.h"
をincludeしている必要があるが、IL2CPPで作成されたコードにはだいたいincludeされてる。
感想
ここまで読まれた人がいたら、ついででいいので読んでおいてほしい。
自分はこう思っている。
・Debug.Logを無効化する方法はいろいろあるが、いまだに完全なものはない。完全なものが欲しい。誰が書いたどんなコードにも適応できる、完全なものが。
・まあ別にstring concatとかToStringとか重たい関数をDebug.Log()のかっこの中で実行してGCがでるくらいいいじゃん、という発想は別にアリだと思っている。
・Debug.Logが一つでもコードに残っているのは悪だ、どんどん消すべき!みたいなのには別に同意しない。
・そんなのは書いたやつの自由だし、なんで製品版ビルドなのにログで損をするんだよという感じ。
・そして、それらとは関係なく、この文脈においてDebug.Logをユーザーに上書きさせることはやはり狂っている。
以上だ。
はい。